/* eslint-disable max-statements */ import type { ParsedUrlQuery } from 'querystring'; import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; import NextImage from 'next/image'; import { useRouter } from 'next/router'; import Script from 'next/script'; import type { HTMLAttributes } from 'react'; import { useIntl } from 'react-intl'; import { ButtonLink, getLayout, Link, PageLayout, Sharing, Spinner, type MetaItemData, } from '../../components'; import { getAllArticlesSlugs, getAllComments, getArticleBySlug, } from '../../services/graphql'; import styles from '../../styles/pages/article.module.scss'; import type { Article, NextPageWithLayout, SingleComment } from '../../types'; import { ROUTES } from '../../utils/constants'; import { getBlogSchema, getFormattedDate, getSchemaJson, getSinglePageSchema, getWebPageSchema, } from '../../utils/helpers'; import { loadTranslation, type Messages } from '../../utils/helpers/server'; import { type OptionalPrismPlugin, useArticle, useBreadcrumb, useComments, usePrism, useReadingTime, useSettings, } from '../../utils/hooks'; type ArticlePageProps = { comments: SingleComment[]; post: Article; slug: string; translation: Messages; }; /** * Article page. */ const ArticlePage: NextPageWithLayout = ({ comments, post, slug, }) => { const { isFallback } = useRouter(); const intl = useIntl(); const article = useArticle({ slug, fallback: post }); const commentsData = useComments({ contentId: article?.id, fallback: comments, }); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title: article?.title ?? '', url: `${ROUTES.ARTICLE}/${slug}`, }); const readingTime = useReadingTime(article?.meta.wordsCount ?? 0, true); const { website } = useSettings(); const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers']; const { attributes, className } = usePrism({ plugins: prismPlugins }); const loadingArticle = intl.formatMessage({ defaultMessage: 'Loading the requested article...', description: 'ArticlePage: loading article message', id: '4iYISO', }); if (isFallback || !article) return {loadingArticle}; const { content, id, intro, meta, title } = article; const { author, commentsCount, cover, dates, seo, thematics, topics } = meta; /** * Retrieve a formatted date (and time). * * @param {string} date - A date string. * @returns {JSX.Element} The formatted date wrapped in a time element. */ const getDate = (date: string): JSX.Element => { const isoDate = new Date(`${date}`).toISOString(); return ; }; const headerMeta: (MetaItemData | undefined)[] = [ author ? { id: 'author', label: intl.formatMessage({ defaultMessage: 'Written by:', description: 'ArticlePage: author label', id: 'MJbZfX', }), value: author.name, } : undefined, { id: 'publication-date', label: intl.formatMessage({ defaultMessage: 'Published on:', description: 'ArticlePage: publication date label', id: 'RecdwX', }), value: getDate(dates.publication), }, dates.update && dates.publication !== dates.update ? { id: 'update-date', label: intl.formatMessage({ defaultMessage: 'Updated on:', description: 'ArticlePage: update date label', id: 'ZAqGZ6', }), value: getDate(dates.update), } : undefined, { id: 'reading-time', label: intl.formatMessage({ defaultMessage: 'Reading time:', description: 'ArticlePage: reading time label', id: 'Gw7X3x', }), value: readingTime, }, thematics ? { id: 'thematics', label: intl.formatMessage({ defaultMessage: 'Thematics:', description: 'ArticlePage: thematics meta label', id: 'CvOqoh', }), value: thematics.map((thematic) => { return { id: `thematic-${thematic.id}`, value: ( {thematic.name} ), }; }), } : undefined, ]; const filteredHeaderMeta = headerMeta.filter( (item): item is MetaItemData => !!item ); const footerMetaLabel = intl.formatMessage({ defaultMessage: 'Read more articles about:', description: 'ArticlePage: footer topics list label', id: '50xc4o', }); const footerMeta: MetaItemData[] = topics ? [ { id: 'more-about', label: footerMetaLabel, value: topics.map((topic) => { return { id: `topic--${topic.id}`, value: ( {topic.logo ? : null}{' '} {topic.name} ), }; }), }, ] : []; const webpageSchema = getWebPageSchema({ description: intro, locale: website.locales.default, slug, title, updateDate: dates.update, }); const blogSchema = getBlogSchema({ isSinglePage: true, locale: website.locales.default, slug, }); const blogPostSchema = getSinglePageSchema({ commentsCount, content, cover: cover?.src, dates, description: intro, id: 'article', kind: 'post', locale: website.locales.default, slug, title, }); const schemaJsonLd = getSchemaJson([ webpageSchema, blogSchema, blogPostSchema, ]); const lineNumbersClassName = className .replace('command-line', '') .replace(/\s\s+/g, ' '); const commandLineClassName = className .replace('line-numbers', '') .replace(/\s\s+/g, ' '); /** * Replace a string with Prism classnames and attributes. * * @param {string} str - The found string. * @returns {string} The classes and attributes. */ const prismClassNameReplacer = (str: string): string => { const wpBlockClassName = 'wp-block-code'; const languageArray = /language-[^\s|"]+/.exec(str); const languageClassName = languageArray ? `${languageArray[0]}` : ''; if ( str.includes('command-line') || (!str.includes('command-line') && str.includes('language-bash')) ) { return `class="${wpBlockClassName} ${commandLineClassName}${languageClassName}" tabindex="0" data-filter-output="#output#`; } return `class="${wpBlockClassName} ${lineNumbersClassName}${languageClassName}" tabindex="0`; }; const contentWithPrismClasses = content.replaceAll( /class="wp-block-code[^"]+/gm, prismClassNameReplacer ); const pageUrl = `${website.url}${slug}`; return ( <> {seo.title} {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */}